package org.hawk.gwt.ppc.loader;

import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import org.hawk.gwt.ppc.utils.Name;
import org.hawk.gwt.ppc.utils.PreprocessorException;

/**
 * Loads java sources from the jar file.
 * @author alex.bereznevatiy@gmail.com
 *
 */
public class JarSourcesLoader implements SourcesLoader {
	private static final String JAR_PROTOCOL = "jar:";
	private static final char QUERY_SEPARATOR = '!';
	private static final char JAR_ENTRY_SEPARATOR = '/';
	private JarFile jarFile;
	private Map<String,List<JarEntry>> packages;
	
	/**
	 * Creates {@link JarSourcesLoader} for the location of
	 * the jar file.
	 * @param jarLocation
	 * @throws IOException
	 */
	public JarSourcesLoader(String jarLocation) throws IOException{
		this(new JarFile(jarLocation));
	}
	
	/**
	 * Creates {@link JarSourcesLoader} for the location of
	 * the jar file.
	 * @param jarLocation
	 * @throws IOException
	 */
	public JarSourcesLoader(File jarLocation) throws IOException {
		this(new JarFile(jarLocation));
	}
	
	/**
	 * Creates {@link JarSourcesLoader} for the jar file specified.
	 * @param jarFile
	 */
	public JarSourcesLoader(JarFile jarFile) {
		if(jarFile==null){
			throw new NullPointerException();
		}
		this.jarFile = jarFile;
	}
	
	/* (non-Javadoc)
	 * @see org.hawk.gwt.ppc.scanner.SourcesLoader#getCompilationUnits(java.lang.String)
	 */
	public List<String> getCompilationUnits(String packageName) {
		if (!Name.isValidPackageName(packageName)) {
			throw new IllegalArgumentException("Bad package: " + String.valueOf(packageName));
		}
		
		readJarIfNecessary();
		List<JarEntry> entries = packages.get(packageName);
		if (entries == null) {
			return Collections.emptyList();
		}
		ArrayList<String> result = new ArrayList<String>();
		for(JarEntry entry : entries) {
			addIfJavaSrc(result,entry,packageName);
		}
		Collections.sort(result);
		result.trimToSize();
		return Collections.unmodifiableList(result);
	}
	
	/* (non-Javadoc)
	 * @see org.hawk.gwt.ppc.scanner.SourcesLoader#getCompilationUnit(java.lang.String)
	 */
	public URL getCompilationUnit(String name) {
		if (name == null || name.length() == 0) {
			return null;
		}
		return getResource(Name.javaToFs(name));
	}

	private void addIfJavaSrc(ArrayList<String> result, JarEntry entry, String packageName) {
		String name = getFileName(entry);
		if(!Name.isJavaSrc(name)){
			return;
		}
		result.add(Name.getChildJavaName(
			packageName,Name.fsToJava(name)));
	}
	/*
	 * (non-Javadoc)
	 * @see org.hawk.gwt.ppc.scanner.SourcesLoader#getResource(java.lang.String)
	 */
	public URL getResource(String name) {
		if(jarFile.getEntry(name)==null){
			return null;
		}
		try{
			StringBuilder uri = new StringBuilder();
			uri.append(JAR_PROTOCOL);
			uri.append(new File(jarFile.getName()).toURI());
			uri.append(QUERY_SEPARATOR);
			uri.append(JAR_ENTRY_SEPARATOR);
			uri.append(name);
			URI url = new URI(uri.toString());
			url.normalize();
			return url.toURL();
		}
		catch(Exception ex){
			return null;
		}
	}
	
	/*
	 * (non-Javadoc)
	 * @see org.hawk.gwt.ppc.scanner.SourcesLoader#getSubpackages(java.lang.String)
	 */
	public List<String> getSubpackages(String packageName) {
		if (!Name.isValidPackageName(packageName)) {
			throw new IllegalArgumentException("Bad package: " + String.valueOf(packageName));
		}
		ArrayList<String> result = new ArrayList<String>();
		for(String pkg : packages.keySet()) {
			if(Name.isDirectSubPackage(pkg,packageName)) {
				result.add(pkg);
			}
		}
		Collections.sort(result);
		result.trimToSize();
		return Collections.unmodifiableList(result);
	}
	
	/* (non-Javadoc)
	 * @see org.hawk.gwt.ppc.loader.SourcesLoader#isExists(java.lang.String)
	 */
	public boolean isExists(String resourceName) {
		readJarIfNecessary();
		return packages.get(resourceName) != null ||
				getCompilationUnit(resourceName) != null;
	}

	/*
	 * (non-Javadoc)
	 * @see org.hawk.gwt.ppc.scanner.SourcesLoader#toUrl()
	 */
	public URL toUrl() throws PreprocessorException {
		try {
			return new File(jarFile.getName()).toURI().toURL();
		}
		catch (MalformedURLException e) {
			throw new PreprocessorException(e);
		}
	}

	private void readJarIfNecessary() {
		if (packages != null) {
			return;
		}
		packages = new HashMap<String, List<JarEntry>>();
		Enumeration<JarEntry> enties = jarFile.entries();
		
		while (enties.hasMoreElements()) {
			JarEntry entry = enties.nextElement();
			String pkg = getPackageName(entry);
			if (packages.get(pkg) == null) {
				String tmp = pkg;
				do {
					packages.put(tmp, new LinkedList<JarEntry>());
					tmp = Name.getParentJavaName(pkg);
				}
				while(tmp.length()>0&&
					packages.get(tmp)==null);
			}
			packages.get(pkg).add(entry);
		}
	}
	
	private String getPackageName(JarEntry entry) {
		int i = entry.getName().lastIndexOf(JAR_ENTRY_SEPARATOR);
		if (i < 0) {
			return "";
		}
		return entry.getName().substring(0, i)
			.replace(JAR_ENTRY_SEPARATOR, Name.PACKAGE_SEPARATOR);
	}
	
	private String getFileName(JarEntry entry) {
		int i = entry.getName().lastIndexOf(JAR_ENTRY_SEPARATOR);
		if (i < 0) {
			return entry.getName();
		}
		return entry.getName().substring(i + 1);
	}
	
	@Override
	public String toString() {
		return "Jar[" + jarFile + "]";
	}
	
	@Override
	public boolean equals(Object obj) {
		if(this == obj) {
			return true;
		}
		if (obj instanceof JarSourcesLoader) {
			JarSourcesLoader loader = (JarSourcesLoader)obj;
			return loader.jarFile.getName()
				.equals(jarFile.getName());
		}
		return false;
	}
}
